﻿using System;
using System.Collections.Generic;
using System.Reflection;
using CLIFile;
using CompileLib.InteropServices.NVIDIA;
using PerformanceTest.CPU;
using StreamDefs;
using PerfCounter;

namespace Scheduling
{
    public class LoopDescriptor
    {
        private long first;
        private long last;
        private long loops;

        public static int SortByStart(LoopDescriptor l1, LoopDescriptor l2)
        {
            return l1.First.CompareTo(l2.First);
        }

        public static int SortByEnd(LoopDescriptor l1, LoopDescriptor l2)
        {
            return l1.Last.CompareTo(l2.Last);
        }

        public long First
        {
            get { return this.first; }
            set { this.first = value; }
        }

        public long Last
        {
            get { return this.last; }
            set { this.last = value; }
        }

        public long Loops
        {
            get { return this.loops; }
            set { this.loops = value; }
        }

        public LoopDescriptor(long first, long last, long loops)
        {
            First = first;
            Last = last;
            Loops = loops;
        }
    }

    public class Scheduler
    {
        private ILCursor _cursor;
        private CLIFileReader cliFileReader;
        private Type classType;
        private OutputStream<int> outArg;
        private InputStream<int>[] inputArgs;
        //contiene le lunghezze degli stream
        private int[] streamsLengths;

        /// <summary>
        /// Finds a method with Kernel attribute
        /// </summary>
        /// <param name="t">The type of the class that should contain a
        /// Kernel definition</param>
        /// <returns>Metadata of the method if found, null otherwise</returns>
        private MethodInfo FindKernel(Type t, string name)
        {
            MethodInfo method = t.GetMethod(name);
            object[] attribs = method.GetCustomAttributes(false);
            if (attribs.Length == 0)
                return null;

            int KernelAttribIndex = Array.FindIndex<Object>(attribs,
                                                     delegate(Object o)
                                                     {
                                                         return o.GetType().Equals(typeof(KernelsAttribute));
                                                     });
            if (KernelAttribIndex == -1)
                return null;
            return method;
        }

        ///// <summary>
        ///// 
        ///// </summary>
        ///// <param name="instr"></param>
        //private void PrintInstruction(ILInstruction instr)
        //{
        //    object o = instr.ResolveParameter(cliFileReader);
        //    if (_cursor.IsCall)
        //    {
        //        Target[] cargs = _cursor.CallLoadArguments();
        //        MethodDesc meth = (MethodDesc)o;
        //        Console.WriteLine(string.Format("{0} to {1} at offset {2:X4}", _cursor.Instr.op, meth.Name, _cursor.Position));
        //        for (int i = 0; i < 1; i++)
        //            Console.WriteLine(string.Format("\tPar {0} at offset {1:X4}", i, cargs[i].Position));
        //    }
        //    else
        //    {
        //        object par = instr.ResolveParameter(cliFileReader);
        //        if ((OPCODES)(_cursor.Instr.op.Value) == OPCODES.Br_S || (OPCODES)(_cursor.Instr.op.Value) == OPCODES.Brtrue_S)
        //            Console.WriteLine("{0:X4}   {1} {2}", _cursor.Position, _cursor.Instr.op, par);
        //        else
        //            Console.WriteLine("{0:X4}   {1} {2}", _cursor.Position, _cursor.Instr.op, par);
        //    }
        //}

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private List<LoopDescriptor> FindLoops()
        {
            List<LoopDescriptor> loops = new List<LoopDescriptor>();
            long iter = 1;
            while (_cursor.Next())
            {
                ILInstruction instr = ILInstruction.Normalize(_cursor.Instr);
                //OPCODES op = (OPCODES)(instr.op.Value);
                OPCODES op = (OPCODES)((instr.op.Value < 0 ? 256 : 0) + (instr.op.Value & 0xFF));
                long currPos = _cursor.Position;
                //PrintInstruction(instr);
                switch (op)
                {
                    case OPCODES.Br:
                    case OPCODES.Br_S:
                    case OPCODES.Brtrue:
                    case OPCODES.Brtrue_S:
                    case OPCODES.Brfalse:
                    case OPCODES.Brfalse_S:
                        //Backward Branch == loop
                        Target tg = (Target)instr.ResolveParameter(cliFileReader);
                        if (tg.Position < currPos)
                            loops.Add(new LoopDescriptor(tg.Position, currPos, iter));
                        break;
                    case OPCODES.Ldarg:
                        iter = streamsLengths[instr.par.iv - 1];
                        break;
                    //case OPCODES.Ldarg_0:
                    //    break;
                    //case OPCODES.Ldarg_1:
                    //    iter = streamLength[0];
                    //    break;
                    //case OPCODES.Ldarg_2:
                    //    iter = streamLength[1];
                    //    break;
                    //case OPCODES.Ldarg_3:
                    //    iter = streamLength[2];
                    //    break;
                    //case OPCODES.Ldarg_S:
                    //case OPCODES.Ldarga:
                    //case OPCODES.Ldarga_S:
                    //    break;
                    case OPCODES.Call:
                    case OPCODES.Calli:
                    case OPCODES.Callvirt:
                        Target[] cargs = _cursor.CallLoadArguments();
                        MethodDesc trg = (MethodDesc)_cursor.Instr.ResolveParameter(cliFileReader);
                        if (!trg.Name.Equals("MoveNext"))
                            iter = 1;
                        break;
                }

            }
            //Ordering loops by execution
            loops.Sort(LoopDescriptor.SortByStart);

            return loops;
        }

        /// <summary>
        /// Makes an analysis of the IL code of the method
        /// </summary>
        private Dictionary<OPCODES, long> ILAnalysis()
        {
            Dictionary<OPCODES, long> InstructionsCount = new Dictionary<OPCODES, long>();
            InstructionsCount.Add(OPCODES.Add, 0);
            InstructionsCount.Add(OPCODES.Sub, 0);
            InstructionsCount.Add(OPCODES.Mul, 0);
            InstructionsCount.Add(OPCODES.Div, 0);
            InstructionsCount.Add(OPCODES.Br, 0);
            InstructionsCount.Add(OPCODES.Br_S, 0);
            InstructionsCount.Add(OPCODES.Brfalse, 0);
            InstructionsCount.Add(OPCODES.Brfalse_S, 0);
            InstructionsCount.Add(OPCODES.Brtrue, 0);
            InstructionsCount.Add(OPCODES.Brtrue_S, 0);
            InstructionsCount.Add(OPCODES.Call, 0);
            InstructionsCount.Add(OPCODES.Calli, 0);
            InstructionsCount.Add(OPCODES.Callvirt, 0);
            InstructionsCount.Add(OPCODES.Ldarg, 0);
            InstructionsCount.Add(OPCODES.Ldarg_0, 0);
            InstructionsCount.Add(OPCODES.Ldarg_1, 0);
            InstructionsCount.Add(OPCODES.Ldarg_2, 0);
            InstructionsCount.Add(OPCODES.Ldarg_3, 0);
            InstructionsCount.Add(OPCODES.Ldarg_S, 0);
            InstructionsCount.Add(OPCODES.Starg, 0);
            InstructionsCount.Add(OPCODES.Starg_S, 0);
            InstructionsCount.Add(OPCODES.Ldloc, 0);
            InstructionsCount.Add(OPCODES.Ldloc_0, 0);
            InstructionsCount.Add(OPCODES.Ldloc_1, 0);
            InstructionsCount.Add(OPCODES.Ldloc_2, 0);
            InstructionsCount.Add(OPCODES.Ldloc_3, 0);
            InstructionsCount.Add(OPCODES.Ldloc_S, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_0, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_1, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_2, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_3, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_4, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_5, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_6, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_7, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_8, 0);
            InstructionsCount.Add(OPCODES.Ldc_I4_S, 0);
            InstructionsCount.Add(OPCODES.Stloc, 0);
            InstructionsCount.Add(OPCODES.Stloc_0, 0);
            InstructionsCount.Add(OPCODES.Stloc_1, 0);
            InstructionsCount.Add(OPCODES.Stloc_2, 0);
            InstructionsCount.Add(OPCODES.Stloc_3, 0);
            InstructionsCount.Add(OPCODES.Stloc_S, 0);
            InstructionsCount.Add(OPCODES.Clt, 0);
            InstructionsCount.Add(OPCODES.Nop, 0);
            InstructionsCount.Add(OPCODES.Ret, 0);


            List<LoopDescriptor> loops = FindLoops();
            List<LoopDescriptor> ends = new List<LoopDescriptor>();
            _cursor.Reset();
            long globalIter = 1;
            long currentIter = 1;
            while (_cursor.Next())
            {
                ILInstruction instr = ILInstruction.Normalize(_cursor.Instr);
                OPCODES op = (OPCODES)(_cursor.Instr.op.Value);
                long currPos = _cursor.Position;
                if (loops.Count != 0 && currPos == loops[0].First)
                {
                    //Entering new loop
                    globalIter *= loops[0].Loops;
                    currentIter = loops[0].Loops;
                    ends.Insert(0, loops[0]);
                    loops.RemoveAt(0);
                }
                if (ends.Count != 0 && currPos == ends[0].Last)
                {
                    //Exiting loop
                    globalIter /= currentIter;
                    ends.RemoveAt(0);
                }
                try
                {
                    InstructionsCount[op] += globalIter;
                }
                catch { }
            }
            return InstructionsCount;
        }

        //public void ScheduleOnCPU()
        //{
        //    object o = Activator.CreateInstance(classType);
        //    MethodInfo mi = classType.GetMethod(method);
        //    object[] param = new object[parameters.Length];
        //    int inputCount = 0;
        //    for (int i = 0; i < parameters.Length; i++)
        //    {
        //        ParameterInfo pi = parameters[i];
        //        if (pi.ParameterType.Name.Equals("InputStream`1"))
        //        {
        //            param[i] = inputArgs[inputCount];
        //            inputArgs[inputCount].MoveNext();
        //            inputCount++;
        //        }
        //        if (pi.ParameterType.Name.Equals("OutputStream`1"))
        //        {
        //            param[i] = outArg;
        //            outArg.MoveNext();
        //        }
        //    }
        //    int count = outArg.Count();
        //    QPerfCounter q = new QPerfCounter(MagTime.msec);
        //    q.Start();
        //    //for (int i = 0; i < count; i++)
        //    //{
        //    //    output[i] = input1[i] + input2[i];//mi.Invoke(o, param);
        //    //}
        //    q.Stop();
        //    Console.WriteLine(q.Duration(1));
        //}

        private void BindParameters(ParameterInfo[] parameters)
        {
            int inputCount = 0;
            streamsLengths = new int[parameters.Length];
            for (int i = 0; i < parameters.Length; i++)
            {
                ParameterInfo pi = parameters[i];
                if (pi.ParameterType.Name.Equals("InputStream`1"))
                {
                    streamsLengths[i] = inputArgs[inputCount++].Count();
                }
                if (pi.ParameterType.Name.Equals("OutputStream`1"))
                {
                    streamsLengths[i] = outArg.Count();
                }
            }
        }

        private void CLIFileInitialization(MethodInfo kernel)
        {
            MethodTableCursor cur = CLIFileReader.FindMethod(kernel);
            CLIFile.MethodBody cb = cur.MethodBody;

            LocalsVarSig locals = cb.LocalsSignature;
            CLIType[] vars = null;
            if (locals != null)
            {
                IEnumerator<CLIType> iterLocal = locals.GetVariables().GetEnumerator();
                vars = new CLIType[locals.Count];
                int l = 0;
                while ((iterLocal.MoveNext()) && (l < vars.Length))
                    vars[l++] = iterLocal.Current;
            }

            MethodSig ms = cur.GetMethodSignature();
            CLIType[] args = new CLIType[ms.Count + 1];
            IEnumerator<CLIType> iter = ms.GetParameters(cur.FindCLIType()).GetEnumerator();
            int m = 0;
            args[m++] = cur.FindCLIType();
            while ((m < args.Length) && (iter.MoveNext()))
                args[m++] = iter.Current;

            _cursor = cb.ILInstructions;
            cliFileReader = _cursor.FileReader;
            _cursor.TrackTargets();
            _cursor.TrackStack(args, vars, kernel.GetMethodBody().MaxStackSize);
        }

        private double GetUnitValue()
        {
            return new CPUPerformance().UnitInMs;

        }

        private void ScanDevices()
        {
            int cudaDevices = 0;
            CUDA.GetDevices(ref cudaDevices);
            if (cudaDevices == 1)
                ;//ScheduleOnCPU();
        }

        /// <summary>
        /// 
        /// </summary>
        public void Schedule(
            object o, 
            string kernelName, 
            OutputStream<int> outStream, 
            params InputStream<int>[] inputStreamList)
        {
            //il tipo degli stream dovrebbe essere qualunque
            classType = o.GetType();
            outArg = outStream;
            inputArgs = inputStreamList;
            long doeSize = outStream.Count();

            MethodInfo kernel = FindKernel(classType, kernelName);

            BindParameters(kernel.GetParameters());

            CLIFileInitialization(kernel);

            Dictionary<OPCODES, long> ic = ILAnalysis();

            ScanDevices();
 
            double unit = GetUnitValue();
            Console.WriteLine("UNIT: {0}", unit);

            GPGPUPerfModel gpgpu = new NVIDIAPerfModel();
            CPUPerfModel cpu = new CPUPerfModel();
            long cpuCost = cpu.ExecutionCost(ic) * doeSize;
            long ib = 0;
            foreach (StreamDefs.InputStream<int> input in inputArgs)
                ib += InputStream<int>.GetSize() * input.Count();
            long gpuCost = gpgpu.GetSingleKernelCost(doeSize, ib, OutputStream<int>.GetSize() * outStream.Count(), ic, unit);
                     
            Console.WriteLine("GPU Cost: {0}", gpuCost);
            Console.WriteLine("CPU Cost: {0}", cpuCost);
            Console.WriteLine("Run on {0}", gpuCost < cpuCost ? "GPU" : "CPU");
        }
    }
}
